/**
 * Copyright (c) 2015, Lucee Association Switzerland. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either 
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public 
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package lucee.runtime;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import lucee.commons.lang.ExceptionUtil;
import lucee.commons.lang.Pair;
import lucee.runtime.component.ComponentPageRef;
import lucee.runtime.component.DataMember;
import lucee.runtime.component.Member;
import lucee.runtime.component.StaticStruct;
import lucee.runtime.config.ConfigPro;
import lucee.runtime.debug.DebugEntryTemplate;
import lucee.runtime.dump.DumpData;
import lucee.runtime.dump.DumpProperties;
import lucee.runtime.dump.DumpTable;
import lucee.runtime.dump.DumpUtil;
import lucee.runtime.dump.SimpleDumpData;
import lucee.runtime.engine.ThreadLocalPageContext;
import lucee.runtime.exp.ExpressionException;
import lucee.runtime.exp.PageException;
import lucee.runtime.exp.PageRuntimeException;
import lucee.runtime.type.Collection;
import lucee.runtime.type.Objects;
import lucee.runtime.type.Struct;
import lucee.runtime.type.UDF;
import lucee.runtime.type.scope.AccessModifier;
import lucee.runtime.type.scope.Variables;
import lucee.runtime.type.util.KeyConstants;
import lucee.runtime.type.util.StructSupport;

public final class StaticScope extends StructSupport implements Variables, Objects, AccessModifier {

	private static final long serialVersionUID = -2692540782121852340L;

	private final StaticScope base;
	private ComponentPageRef cpr;
	private final int dataMemberDefaultAccess;
	private final ComponentImpl c;

	public StaticScope(StaticScope base, ComponentImpl c, ComponentPageRef cpr, int dataMemberDefaultAccess) {
		this.base = base;
		this.cpr = cpr;
		this.c = c;
		this.dataMemberDefaultAccess = dataMemberDefaultAccess;
	}

	public PageSource getPageSource(PageContext pc) throws PageException {
		return this.cpr.get(pc).getPageSource();
	}

	@Override
	public int size() {
		ComponentPageImpl cp = cpr.get(null, null);
		int s = cp == null ? 0 : cp.getStaticStruct().size();
		return (base == null) ? s : base.size() + s;

	}

	public Member _remove(PageContext pc, Key key) throws PageException {
		// does the current struct has this key
		StaticStruct ss = cpr.get(pc).getStaticStruct();
		Member m = ss.get(key);
		if (m != null) {
			if (m.getModifier() == Member.MODIFIER_FINAL) throw new ExpressionException(
					"Cannot remove key [" + key + "] in static scope from component [" + cpr.get(pc).getComponentName() + "], that member is set to final");

			if (!c.isAccessible(ThreadLocalPageContext.get(pc), m.getAccess())) throw notExisting(key);
			return ss.remove(key);
		}
		// if not the parent (inside the static constructor we do not remove keys from base static scopes)

		if (base != null && !c.insideStaticConstrThread.get()) return base._remove(pc, key);
		return null;
	}

	@Override
	public Object remove(Key key) throws PageException {
		Member m = _remove(ThreadLocalPageContext.get(), key);
		if (m != null) return m.getValue();
		return null;
	}

	@Override
	public Object removeEL(Key key) {
		try {
			return remove(key);
		}
		catch (Throwable t) {
			ExceptionUtil.rethrowIfNecessary(t);
			return null;
		}
	}

	@Override
	public void clear() {
		if (base != null) base.clear();
		ComponentPageImpl cp = cpr.get(null, null);
		if (cp != null) cp.getStaticStruct().clear();
	}

	private Member _get(PageContext pc, Key key, Member defaultValue) {
		// does the current struct has this key
		ComponentPageImpl cp = cpr.get(pc, null);
		StaticStruct ss = cp == null ? null : cp.getStaticStruct();
		if (ss != null && !ss.isEmpty()) {
			Member m = ss.get(key);

			if (m != null) {
				if (c.isAccessible(pc, m)) return m;
				return null;
			}
		}
		// if not the parent
		if (base != null) return base._get(pc, key, defaultValue);
		return null;
	}

	private Pair<Member, ComponentImpl> _getWithBase(PageContext pc, Key key, Member defaultValue) {
		// does the current struct has this key
		ComponentPageImpl cp = cpr.get(pc, null);
		StaticStruct ss = cp == null ? null : cp.getStaticStruct();
		if (ss != null && !ss.isEmpty()) {
			Member m = ss.get(key);

			if (m != null) {
				if (c.isAccessible(pc, m)) return new Pair<Member, ComponentImpl>(m, c);
				return null;
			}
		}
		// if not the parent
		if (base != null) return base._getWithBase(pc, key, defaultValue);
		return null;
	}

	@Override
	public Object get(Key key) throws PageException {
		return get((PageContext) null, key);
	}

	@Override
	public Object get(PageContext pc, Key key) throws PageException {
		if (key.equalsIgnoreCase(KeyConstants._STATIC)) return c.top._static;

		Member m = _get(ThreadLocalPageContext.get(pc), key, null);
		if (m != null) return m.getValue();
		throw notExisting(key);
	}

	@Override
	public Object get(Key key, Object defaultValue) {
		return get(null, key, defaultValue);
	}

	@Override
	public Object get(PageContext pc, Key key, Object defaultValue) {
		if (key.equalsIgnoreCase(KeyConstants._STATIC)) return c.top._static;
		Member m = _get(ThreadLocalPageContext.get(pc), key, null);
		if (m != null) return m.getValue();
		return defaultValue;
	}

	public Member getMember(PageContext pc, Key key, Member defaultValue) {
		return _get(ThreadLocalPageContext.get(pc), key, null);
	}

	private Member _setIfExists(PageContext pc, Key key, Object value, int access, int modifier) throws PageException {
		// does the current struct has this key
		Pair<Member, ComponentImpl> p = null;
		if (base != null) {
			p = base._getWithBase(pc, key, null);
		}

		if (p != null) {
			if (p.getName().getModifier() == Member.MODIFIER_FINAL) throw new ExpressionException("Attempt to modify a 'final' member [" + key
					+ "]  within the 'static' scope of the component [" + c._getPageSource().getDisplayPath() + "]. This member is inherited from the base component ["
					+ p.getValue()._getPageSource().getDisplayPath() + "] where it is declared as 'final' and cannot be overridden.");

			return _set(pc, p.getName(), key, value, access, modifier);
		}

		// if not the parent (we only do this if we are outside the static constructor)
		if (base != null && !c.insideStaticConstrThread.get()) return base._setIfExists(pc, key, value, access, modifier);
		return null;
	}

	private Member _set(PageContext pc, Member existing, Key key, Object value, int access, int modifier) throws PageException {
		if (value instanceof Member) {
			return cpr.get(pc).getStaticStruct().put(key, (Member) value);
		}

		// check if user has access
		if (!c.isAccessible(pc, existing != null ? existing.getAccess() : dataMemberDefaultAccess)) throw notExisting(key);

		// set
		return cpr.get(pc).getStaticStruct().put(key,
				new DataMember(existing != null ? existing.getAccess() : access, existing != null ? existing.getModifier() : modifier, value));
	}

	@Override
	public Object set(Key propertyName, Object value) throws PageException {
		return set(null, propertyName, value);
	}

	@Override
	public Object set(PageContext pc, Key key, Object value) throws PageException {
		pc = ThreadLocalPageContext.get(pc);
		Member m = _setIfExists(pc, key, value, dataMemberDefaultAccess, Member.MODIFIER_NONE);
		if (m != null) return m.getValue();
		// if not exists set to current
		m = _set(pc, null, key, value, dataMemberDefaultAccess, Member.MODIFIER_NONE);
		if (m != null) return m.getValue();
		return null;
	}

	@Override
	public Object set(PageContext pc, Key key, Object value, int access, int modifier) throws PageException {
		pc = ThreadLocalPageContext.get(pc);
		Member m = _setIfExists(pc, key, value, access, modifier);
		if (m != null) return m.getValue();
		// if not exists set to current
		m = _set(pc, null, key, value, access, modifier);
		if (m != null) return m.getValue();
		return null;
	}

	@Override
	public Object setEL(Key key, Object value) {
		return setEL(null, key, value);
	}

	@Override
	public Object setEL(PageContext pc, Key propertyName, Object value) {
		try {
			return set(ThreadLocalPageContext.get(pc), propertyName, value);
		}
		catch (PageException e) {
			return null;
		}
	}

	@Override
	public Collection duplicate(boolean deepCopy) {
		return this;
	}

	@Override
	public final boolean containsKey(Key key) {
		if (base != null && base.containsKey(key)) return true;
		ComponentPageImpl cp = cpr.get(null, null);
		return cp != null && cp.getStaticStruct().containsKey(key);
	}

	@Override
	public final boolean containsKey(PageContext pc, Key key) {
		if (base != null && base.containsKey(pc, key)) return true;
		ComponentPageImpl cp = cpr.get(null, null);
		return cp != null && cp.getStaticStruct().containsKey(key);
	}

	@Override
	public Key[] keys() {
		Set<Key> keys = _entries(new HashMap<Key, Object>(), c.getAccess(ThreadLocalPageContext.get())).keySet();
		return keys.toArray(new Key[keys.size()]);
	}

	@Override
	public Iterator<Key> keyIterator() {
		return _entries(new HashMap<Key, Object>(), c.getAccess(ThreadLocalPageContext.get())).keySet().iterator();
	}

	@Override
	public Iterator<Object> valueIterator() {
		return _entries(new HashMap<Key, Object>(), c.getAccess(ThreadLocalPageContext.get())).values().iterator();
	}

	@Override
	public Iterator<Entry<Key, Object>> entryIterator() {
		return _entries(new HashMap<Key, Object>(), c.getAccess(ThreadLocalPageContext.get())).entrySet().iterator();
	}

	public Iterator<Entry<Key, Object>> entryIterator(int access) {
		return _entries(new HashMap<Key, Object>(), access).entrySet().iterator();
	}

	Map<Key, Object> _entries(Map<Key, Object> map, int access) {
		// call parent
		if (base != null) base._entries(map, access);

		// fill accessable keys
		ComponentPageImpl cp = cpr.get(null, null);
		StaticStruct ss = cp == null ? null : cp.getStaticStruct();
		if (ss != null) {
			Iterator<Entry<Key, Member>> it = ss.entrySet().iterator();
			Entry<Key, Member> e;
			while (it.hasNext()) {
				e = it.next();
				if (e.getValue().getAccess() <= access) map.put(e.getKey(), e.getValue().getValue());
			}
		}
		return map;
	}

	private Map<Key, Member> all(Map<Key, Member> map) {
		// call parent
		if (base != null) base.all(map);

		// fill accessable keys
		ComponentPageImpl cp = cpr.get(null, null);
		StaticStruct ss = cp == null ? null : cp.getStaticStruct();
		if (ss != null) {
			Iterator<Entry<Key, Member>> it = ss.entrySet().iterator();
			Entry<Key, Member> e;
			while (it.hasNext()) {
				e = it.next();
				map.put(e.getKey(), e.getValue());
			}
		}
		return map;
	}

	@Override
	public Object call(PageContext pc, Key key, Object[] args) throws PageException {
		Member m = _get(pc, key, null);
		if (m instanceof UDF) {
			return _call(pc, key, ((UDF) m), null, args);
		}
		throw notExisting(key);
	}

	@Override
	public Object callWithNamedValues(PageContext pc, Key key, Struct args) throws PageException {
		Member m = _get(pc, key, null);
		if (m instanceof UDF) {
			return _call(pc, key, ((UDF) m), args, null);
		}
		throw notExisting(key);
	}

	Object _call(PageContext pc, Collection.Key calledName, UDF udf, Struct namedArgs, Object[] args) throws PageException {

		Object rtn = null;
		Variables parent = null;

		// INFO duplicate code is for faster execution -> less contions

		// debug yes
		if (((PageContextImpl) pc).hasDebugOptions(ConfigPro.DEBUG_TEMPLATE)) {
			DebugEntryTemplate debugEntry = pc.getDebugger().getEntry(pc, cpr.get(pc).getPageSource(), udf.getFunctionName());// new DebugEntry(src,udf.getFunctionName());
			long currTime = pc.getExecutionTime();
			long time = System.nanoTime();

			try {
				parent = beforeStaticCall(pc, c, this);
				if (args != null) rtn = udf.call(pc, calledName, args, true);
				else rtn = udf.callWithNamedValues(pc, calledName, namedArgs, true);
			}
			finally {
				if (parent != null) afterStaticCall(pc, c, parent);
				long diff = ((System.nanoTime() - time) - (pc.getExecutionTime() - currTime));
				pc.setExecutionTime(pc.getExecutionTime() + diff);
				debugEntry.updateExeTime(diff);
			}
		}

		// debug no
		else { // this.cp._static
			try {
				parent = beforeStaticCall(pc, c, this);
				if (args != null) rtn = udf.call(pc, calledName, args, true);
				else rtn = udf.callWithNamedValues(pc, calledName, namedArgs, true);
			}
			finally {
				if (parent != null) afterStaticCall(pc, c, parent);
			}
		}
		return rtn;
	}

	@Override
	public boolean isInitalized() {
		return true;
	}

	@Override
	public void initialize(PageContext pc) {

	}

	@Override
	public void release(PageContext pc) {

	}

	@Override
	public int getType() {
		return SCOPE_VARIABLES;
	}

	@Override
	public String getTypeAsString() {
		return "variables";
	}

	@Override
	public void setBind(boolean bind) {
	}

	@Override
	public boolean isBind() {
		return true;
	}

	@Override
	public DumpData toDumpData(PageContext pageContext, int maxlevel, DumpProperties dp) {
		int access = c.getAccess(pageContext);

		ComponentPageImpl cp = cpr.get(pageContext, null);
		String cfcName = cp != null ? cp.getComponentName() : "unknown"; // unlikely ever happen

		DumpTable table = new DumpTable("component", "#99cc99", "#ccffcc", "#000000");
		table.setTitle("Static Scope from Component " + cfcName);
		table.setComment("Only the functions and data members that are accessible from your location are displayed");

		DumpTable content = _toDumpData(c.top, pageContext, maxlevel, dp, access);
		if (!content.isEmpty()) table.appendRow(1, new SimpleDumpData(""), content);
		return table;
	}

	DumpTable _toDumpData(ComponentImpl ci, PageContext pc, int maxlevel, DumpProperties dp, int access) {
		maxlevel--;

		DumpTable[] accesses = new DumpTable[4];
		accesses[Component.ACCESS_PRIVATE] = new DumpTable("#ff6633", "#ff9966", "#000000");
		accesses[Component.ACCESS_PRIVATE].setTitle("private");
		accesses[Component.ACCESS_PRIVATE].setWidth("100%");

		accesses[Component.ACCESS_PACKAGE] = new DumpTable("#ff9966", "#ffcc99", "#000000");
		accesses[Component.ACCESS_PACKAGE].setTitle("package");
		accesses[Component.ACCESS_PACKAGE].setWidth("100%");

		accesses[Component.ACCESS_PUBLIC] = new DumpTable("#ffcc99", "#ffffcc", "#000000");
		accesses[Component.ACCESS_PUBLIC].setTitle("public");
		accesses[Component.ACCESS_PUBLIC].setWidth("100%");

		accesses[Component.ACCESS_REMOTE] = new DumpTable("#ccffcc", "#ffffcc", "#000000");
		accesses[Component.ACCESS_REMOTE].setTitle("remote");
		accesses[Component.ACCESS_REMOTE].setWidth("100%");

		Iterator<Entry<Key, Member>> it = all(new HashMap<Key, Member>()).entrySet().iterator();
		Entry<Key, Member> e;
		while (it.hasNext()) {
			e = it.next();
			int a = access(pc, e.getValue().getAccess());

			DumpTable box = accesses[a];
			Object o = e.getValue().getValue();

			if (DumpUtil.keyValid(dp, maxlevel, e.getKey())) {
				box.appendRow(1, new SimpleDumpData(e.getKey().getString()), DumpUtil.toDumpData(o, pc, maxlevel, dp));
			}
		}

		DumpTable table = new DumpTable("#ffffff", "#cccccc", "#000000");

		if (!accesses[Component.ACCESS_REMOTE].isEmpty()) {
			table.appendRow(0, accesses[Component.ACCESS_REMOTE]);
		}
		if (!accesses[Component.ACCESS_PUBLIC].isEmpty()) {
			table.appendRow(0, accesses[Component.ACCESS_PUBLIC]);
		}
		if (!accesses[Component.ACCESS_PACKAGE].isEmpty()) {
			table.appendRow(0, accesses[Component.ACCESS_PACKAGE]);
		}
		if (!accesses[Component.ACCESS_PRIVATE].isEmpty()) {
			table.appendRow(0, accesses[Component.ACCESS_PRIVATE]);
		}
		return table;
	}

	private int access(PageContext pc, int access) {
		if (access > -1) return access;
		return pc.getConfig().getComponentDataMemberDefaultAccess();
	}

	public Component getComponent() {
		return c;
	}

	public static Variables beforeStaticConstructor(PageContext pc, ComponentImpl c, StaticScope ss) {
		c.insideStaticConstrThread.set(Boolean.TRUE);
		Variables parent = pc.variablesScope();
		if (parent != ss) {
			pc.setVariablesScope(ss);
			return parent;
		}
		return null;
	}

	public static void afterStaticConstructor(PageContext pc, ComponentImpl c, Variables parent) {
		c.insideStaticConstrThread.set(Boolean.FALSE);
		if (parent != null) pc.setVariablesScope(parent);
	}

	public static Variables beforeStaticCall(PageContext pc, ComponentImpl c, StaticScope ss) {
		Variables parent = pc.variablesScope();
		// only if we are not already in that context
		if (parent != ss) {
			pc.setVariablesScope(ss);
			return parent;
		}
		return null;

	}

	public static void afterStaticCall(PageContext pc, ComponentImpl c, Variables parent) {
		pc.setVariablesScope(parent);
	}

	private ExpressionException notExisting(Collection.Key key) {
		ComponentPageImpl cp = cpr.get(null, null);
		String cfcName = cp != null ? cp.getComponentName() : "unknown"; // unlikely ever happen
		return new ExpressionException(ExceptionUtil.similarKeyMessage(this, key.getString(), "static member", "static members", "Component [" + cfcName + "]", true));
	}

	public long index() {
		try {
			ComponentPageImpl cp = cpr.get(null);
			return cp.getStaticStruct().index();
		}
		catch (PageException e) {
			throw new PageRuntimeException(e);
		}
	}

	@Override
	public final int hashCode() {
		return java.util.Objects.hash(c);
	}
}